Add plugin NewsNow v1.0.0#298
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the 'NewsNow' plugin, which provides real-time news and multi-platform hot search subscriptions. The implementation includes frontend assets, configuration files, and a Node.js-based preload script for handling API requests. However, several critical security and stability issues must be addressed. Specifically, there are two XSS vulnerabilities in the frontend JavaScript where remote API data and URL variables are rendered directly into the DOM without HTML escaping. Additionally, the preload script's redirect handling lacks a maximum limit, which could result in infinite redirect loops and denial of service.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| </div>`}function ee(e,t={}){var p,b;const{preview:n=!1}=t,o=x[e];if(!o)return"";const a=g.get(e)??{},r=i.focusSources.includes(e),s=O===e,c=a.error?a.error:a.loading?"加载中...":(p=a.data)!=null&&p.updatedTime?`${ie(a.data.updatedTime)}更新`:"",u=((b=a.data)==null?void 0:b.items)??[],d=u.length>0?o.type==="hottest"?`<ol class="nn-list-hot">${u.map((l,f)=>{const m=$({sourceId:e,id:l.id}),y=X(i.favorites,m),S=l.url||"#";return`<li> | ||
| <a href="#" class="nn-fav-link" data-url="${encodeURIComponent(S)}" | ||
| data-fav-source="${e}" data-fav-id="${l.id}" | ||
| data-fav-title="${encodeURIComponent(l.title)}" data-fav-url="${encodeURIComponent(S)}"> | ||
| <span class="nn-rank">${f+1}</span> | ||
| <span>${l.title}</span> | ||
| <span class="nn-item-star ${y?"active":""}" data-fav-source="${e}" data-fav-id="${l.id}" data-fav-title="${encodeURIComponent(l.title)}" data-fav-url="${encodeURIComponent(S)}">${y?"★":"☆"}</span> | ||
| </a> | ||
| </li>`}).join("")}</ol>`:`<ol class="nn-list-timeline">${u.map(l=>{var S,E;const f=l.url||"#",m=l.id||`${l.title}-${l.pubDate||""}`;return`<li> | ||
| <div class="meta"><span>${ie(l.pubDate||((S=l.extra)==null?void 0:S.date))}</span><span>${((E=l.extra)==null?void 0:E.info)||""}</span></div> | ||
| <a href="#" class="nn-fav-link" data-url="${encodeURIComponent(f)}" | ||
| data-fav-source="${e}" data-fav-id="${m}" | ||
| data-fav-title="${encodeURIComponent(l.title)}" data-fav-url="${encodeURIComponent(f)}">${l.title}</a> | ||
| </li>`}).join("")}</ol>`:a.error?`<div class="nn-card-error">${a.error}</div>`:'<div class="nn-empty" style="padding:16px">暂无数据</div>',h=o.color||"slate";return` |
There was a problem hiding this comment.
在渲染卡片内容时,直接将远程 API 返回的 l.title 插入到 HTML 模板中(例如 <span>${l.title}</span>),并且没有进行任何 HTML 转义。如果第三方新闻源或热搜接口返回了包含恶意 HTML/JavaScript 脚本的标题(例如 <img src=x onerror=alert(1)>),在通过 innerHTML 渲染时会直接触发跨站脚本攻击(XSS)。
由于该插件运行在 ZTools 客户端环境,XSS 漏洞可能会被利用来调用敏感的客户端 API,造成严重的安全隐患。
建议修复方案:
在将数据插入 HTML 之前,必须对所有来自远程 API 的动态文本(如 l.title、l.extra 等)进行 HTML 实体转义。可以实现一个简单的转义函数,例如:
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>'"/]/g, tag => ({
'&': '&',
'<': '<',
'>': '>',
"'": ''',
'"': '"',
'/': '/'
}[tag] || tag));
}然后在模板字符串中使用 escapeHtml(l.title) 代替直接的 ${l.title}。
| function requestJson(urlString) { | ||
| return new Promise((resolve, reject) => { | ||
| let url; | ||
| try { | ||
| url = new URL(urlString); | ||
| } catch (error) { | ||
| reject(error); | ||
| return; | ||
| } | ||
|
|
||
| const origin = `${url.protocol}//${url.host}`; | ||
| const lib = url.protocol === "https:" ? https : http; | ||
| const options = { | ||
| hostname: url.hostname, | ||
| port: url.port || (url.protocol === "https:" ? 443 : 80), | ||
| path: `${url.pathname}${url.search}`, | ||
| method: "GET", | ||
| headers: buildHeaders(origin), | ||
| }; | ||
|
|
||
| const req = lib.request(options, (res) => { | ||
| if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { | ||
| requestJson(new URL(res.headers.location, urlString).toString()).then(resolve).catch(reject); | ||
| return; | ||
| } | ||
| let body = ""; | ||
| res.setEncoding("utf8"); | ||
| res.on("data", (chunk) => { | ||
| body += chunk; | ||
| }); | ||
| res.on("end", () => { | ||
| if (res.statusCode && res.statusCode >= 400) { | ||
| reject(new Error(parseErrorMessage(body, res.statusCode))); | ||
| return; | ||
| } | ||
| try { | ||
| const json = JSON.parse(body); | ||
| if (json.error) { | ||
| reject(new Error(json.message || json.statusMessage || "请求失败")); | ||
| return; | ||
| } | ||
| resolve(json); | ||
| } catch (error) { | ||
| reject(new Error("响应不是有效 JSON")); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| req.on("error", reject); | ||
| req.on("timeout", () => { | ||
| req.destroy(); | ||
| reject(new Error("请求超时")); | ||
| }); | ||
| req.setTimeout(20000); | ||
| req.end(); | ||
| }); | ||
| } |
There was a problem hiding this comment.
在处理 HTTP 重定向时,缺少对重定向次数的限制,这可能会导致无限重定向循环(例如 A -> B -> A),从而引发栈溢出或请求挂起(拒绝服务)。此外,也应当限制重定向的目标协议仅为 http: 或 https:,以防止潜在的协议走私或非预期行为。
建议引入一个重定向计数器(例如最大允许 5 次重定向),并对 URL 协议进行校验。
function requestJson(urlString, redirectCount = 0) {
return new Promise((resolve, reject) => {
if (redirectCount > 5) {
reject(new Error("重定向次数过多"));
return;
}
let url;
try {
url = new URL(urlString);
} catch (error) {
reject(error);
return;
}
if (url.protocol !== "http:" && url.protocol !== "https:") {
reject(new Error("不支持的协议类型"));
return;
}
const origin = `${url.protocol}//${url.host}`;
const lib = url.protocol === "https:" ? https : http;
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === "https:" ? 443 : 80),
path: `${url.pathname}${url.search}`,
method: "GET",
headers: buildHeaders(origin),
};
const req = lib.request(options, (res) => {
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
const redirectUrl = new URL(res.headers.location, urlString).toString();
requestJson(redirectUrl, redirectCount + 1).then(resolve).catch(reject);
return;
}
let body = "";
res.setEncoding("utf8");
res.on("data", (chunk) => {
body += chunk;
});
res.on("end", () => {
if (res.statusCode && res.statusCode >= 400) {
reject(new Error(parseErrorMessage(body, res.statusCode)));
return;
}
try {
const json = JSON.parse(body);
if (json.error) {
reject(new Error(json.message || json.statusMessage || "请求失败"));
return;
}
resolve(json);
} catch (error) {
reject(new Error("响应不是有效 JSON"));
}
});
});
req.on("error", reject);
req.on("timeout", () => {
req.destroy();
reject(new Error("请求超时"));
});
req.setTimeout(20000);
req.end();
});
}| <div class="nn-webview"> | ||
| <div class="nn-webview-toolbar"> | ||
| <button type="button" data-action="webview-close">← 返回</button> | ||
| <span class="nn-webview-title" title="${e}">${e}</span> | ||
| <button type="button" data-action="webview-external">外部</button> | ||
| </div> | ||
| <iframe class="nn-webview-frame" src="${e}" referrerpolicy="no-referrer"></iframe> | ||
| </div> |
There was a problem hiding this comment.
在 en(e) 函数中,直接将 URL 变量 e 插入到 iframe 的 src 属性和 span 的 title 属性中,并通过 innerHTML 动态渲染。如果 URL 包含双引号或恶意字符(例如 https://example.com/" onload="alert(1)),将会导致 HTML 属性注入和跨站脚本攻击(XSS)。
建议修复方案:
避免直接使用模板字符串拼接 HTML 并通过 innerHTML 渲染。建议使用 document.createElement 动态创建 DOM 元素,并通过安全的方式(如 element.src = e 和 element.textContent = e)设置属性和文本内容,或者在拼接前对 e 进行严格的 HTML 属性转义。
|
感谢pr,目前插件仓库仅支持源码提交,编译后的不支持 |


插件信息
本次变更
截图 / 演示
自检清单
plugins/newsnow/目录此 PR 由 ztools-plugin-cli 自动管理:每次
ztools publish在分支上追加一个 commit,PR 链接保持不变。